ひとりNavigation API Advent Calendar 15日目
https://gyazo.com/cc571bdfa0e21efa77b607e6297ea44b
navigationApiHistory.tsに処理がある
code:ts
import { createNavigationApiHistory } from './navigationApiHistory.ts'
code:ts
const history = window.navigation
? createNavigationApiHistory()
: createWebHistory()
const router = createRouter({
history,
routes,
})
code:ts
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base)
const historyNavigation = useHistoryStateNavigation(base)
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}
useHistoryStateNavigationはpush/replaceの実装
pushメソッドは、ブラウザのhistory.pushState()を呼び出して新しい履歴エントリを追加する 内部的には、changeLocation関数がHistory APIのpushStateまたはreplaceStateを実際に呼び出す code:ts
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
/**
* if a base tag is provided, and we are on a normal domain, we have to
* respect the provided base attribute because pushState() will use it and
* potentially erase anything before the # like at
* /folder/# but a base of / would erase the /folder/ section. If
* there is no host, the <base> tag makes no sense and if there isn't a
* base tag we can just use everything after the #.
*/
const hashIndex = base.indexOf('#')
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// Force the navigation, this also resets the call count
}
}
NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
ここでも見かけた
エラーが発生した場合はlocation.assign()やlocation.replace()にフォールバックする useHistoryListenersはpopstateイベントの管理 popStateHandlerが実装されており、ユーザーがブラウザの戻る/進むボタンを押すと 1. 現在の位置を計算
2. 履歴の状態を更新
3. 登録されたすべてのリスナーに通知
の順番で処理を行う
code:3の処理.ts
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
HistoryStateの型をextendsしている
code:ts
export type HistoryStateValue =
| string
| number
| boolean
| null
| undefined
| HistoryState
| HistoryStateArray
export interface HistoryState {
}
このオブジェクトには以下の情報が含まれる
back: 前の履歴位置
current: 現在の履歴位置
forward: 次の履歴位置
position: 履歴内の位置番号
replaced: replaceStateで置き換えられたかどうか
scroll: スクロール位置
ページが非表示になる際(pagehideイベントやvisibilitychangeイベント)に、現在のスクロール位置を履歴の状態に保存してリスナーに登録している code:ts
function beforeUnloadListener() {
if (document.visibilityState === 'hidden') {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
}
code:ts
// note: iOS safari does not fire beforeunload, so we
// use pagehide and visibilitychange instead
window.addEventListener('pagehide', beforeUnloadListener)
document.addEventListener('visibilitychange', beforeUnloadListener)
https://gyazo.com/4cf2c46cdf5b1ceee06e904e4b803f40
覚えられん…
ところで
While it's not recommended, you can use this mode inside Browser applications but note there will be no history, meaning you won't be able to go back or forward.
推奨はされませんが、このモードをブラウザアプリケーション内で使用することは可能です。ただし、履歴は保存されないため、戻る/進む操作はできません。 どういうこと...?
code:ts
if (event.navigationType === 'traverse') {
const fromIndex = window.navigation.currentEntry?.index ?? -1
const toIndex = event.destination.index
const delta = fromIndex === -1 ? 0 : toIndex - fromIndex
info = {
type: 'pop', // 'traverse' maps to 'pop' in vue-router's terminology.
direction: delta > 0 ? 'forward' : 'back',
delta,
}
}
else if (event.navigationType === 'push' || event.navigationType === 'replace') {
info = {
type: event.navigationType,
direction: '', // No specific direction for push/replace.
delta: event.navigationType === 'push' ? 1 : 0,
}
}
else {
// For 'reload' or other types, we ignore and don't notify listeners.
return
}
navigationTypeにあわせた処理
code:ts
const url = new URL(event.destination.url)
const to = url.pathname + url.search + url.hash
const from = location.value
// We intercept the navigation so vue-router can handle the view update.
event.intercept({
handler: async () => {
location.value = to
state.value = event.destination.getState() as HistoryState
// Notify vue-router listeners with the enriched navigation info.
listeners.forEach(listener => listener(to, from, info))
},
})
code:ts
window.navigation.addEventListener('navigate', handleNavigate)
push、replace、go、listen、destroyメソッドをもつhistoryオブジェクト code:ts
push(to: string, data?: HistoryState) {
window.navigation.navigate(to, { state: data, history: 'push' })
},
replace(to: string, data?: HistoryState) {
window.navigation.navigate(to, { state: data, history: 'replace' })
},
code:ts
go(delta: number) {
// Case 1: go(0) should trigger a reload.
if (delta === 0) {
window.navigation.reload()
return
}
// Get the current state safely, without using non-null assertions ('!').
const entries = window.navigation.entries()
const currentIndex = window.navigation.currentEntry?.index
// If we don't have a current index, we can't proceed.
if (currentIndex === undefined) {
return
}
// Calculate the target index in the history stack.
const targetIndex = currentIndex + delta
// Validate that the target index is within the bounds of the entries array.
// This is the key check that prevents runtime errors.
if (targetIndex >= 0 && targetIndex < entries.length) {
// Each history entry has a unique 'key'. We get the key for our target entry...
// Safely get the target entry from the array.
// Add a check to ensure the entry is not undefined before accessing its key.
// This satisfies TypeScript's strict checks.
if (targetEntry) {
window.navigation.traverseTo(targetEntry.key)
}
else {
// This case is unlikely if the index check passed, but it adds robustness.
console.warn(go(${delta}) failed: No entry found at index ${targetIndex}.)
}
}
else {
console.warn(go(${delta}) failed: target index ${targetIndex} is out of bounds.)
}
},
https://gyazo.com/3a7fd7f66965d137e541e0e94355baa2